"""
Tests for Document Actions Permission System

These tests verify that document_corpus_actions follows the least-privilege model.
This ensures proper permission inheritance from documents and corpuses.

Permission Model:
- Document permissions are primary
- Corpus permissions are secondary
- Effective permission = MIN(document_permission, corpus_permission)

Issue: Permission audit remediation
"""

from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from django.test import TestCase

from opencontractserver.corpuses.models import Corpus, CorpusAction
from opencontractserver.documents.models import Document
from opencontractserver.documents.query_optimizer import DocumentActionsQueryOptimizer
from opencontractserver.extracts.models import Fieldset
from opencontractserver.types.enums import PermissionTypes
from opencontractserver.utils.permissioning import set_permissions_for_obj_to_user

User = get_user_model()


class TestDocumentActionsPermissions(TestCase):
    """Tests for document actions with proper permission inheritance."""

    def setUp(self):
        """Create test scenario with document, corpus, and corpus actions."""
        self.owner = User.objects.create_user(
            username="owner",
            email="owner@example.com",
            password="testpass123",
        )
        self.reader = User.objects.create_user(
            username="reader",
            email="reader@example.com",
            password="testpass123",
        )
        self.outsider = User.objects.create_user(
            username="outsider",
            email="outsider@example.com",
            password="testpass123",
        )

        # Create corpus and document
        self.corpus = Corpus.objects.create(
            title="Test Corpus",
            creator=self.owner,
            is_public=False,
        )
        self.document = Document.objects.create(
            title="Test Document",
            creator=self.owner,
            is_public=False,
        )

        # Give reader READ permission on both corpus and document
        set_permissions_for_obj_to_user(
            self.reader,
            self.corpus,
            [PermissionTypes.READ],
        )
        set_permissions_for_obj_to_user(
            self.reader,
            self.document,
            [PermissionTypes.READ],
        )

        # Create a fieldset for corpus action
        self.fieldset = Fieldset.objects.create(
            name="Test Fieldset",
            description="Test Fieldset Description",
            creator=self.owner,
        )

        # Create a corpus action
        self.corpus_action = CorpusAction.objects.create(
            name="Test Action",
            corpus=self.corpus,
            fieldset=self.fieldset,
            trigger="add_document",
            creator=self.owner,
        )

    def test_owner_can_see_document_actions(self):
        """
        GIVEN: A document and corpus owner with full permissions
        WHEN: Owner queries for document actions
        THEN: Corpus actions should be visible
        """
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=self.owner,
            document_id=self.document.id,
            corpus_id=self.corpus.id,
        )

        self.assertIn(
            self.corpus_action,
            actions["corpus_actions"],
            "Owner should see corpus actions",
        )

    def test_reader_with_permission_can_see_corpus_actions(self):
        """
        GIVEN: A user (reader) with explicit READ permission on document AND corpus
        WHEN: Reader queries for document actions
        THEN: Reader should see corpus actions they have permission to access
        """
        # Give reader read permission on corpus action
        set_permissions_for_obj_to_user(
            self.reader,
            self.corpus_action,
            [PermissionTypes.READ],
        )

        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=self.reader,
            document_id=self.document.id,
            corpus_id=self.corpus.id,
        )

        self.assertIn(
            self.corpus_action,
            actions["corpus_actions"],
            "Reader with explicit permission should see corpus actions",
        )

    def test_outsider_cannot_see_any_document_actions(self):
        """
        GIVEN: A user (outsider) with NO permissions on document or corpus
        WHEN: Outsider queries for document actions
        THEN: All actions should be empty (permission denied)
        """
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=self.outsider,
            document_id=self.document.id,
            corpus_id=self.corpus.id,
        )

        self.assertEqual(
            actions["corpus_actions"],
            [],
            "Outsider should NOT see any corpus actions",
        )
        self.assertEqual(
            actions["extracts"],
            [],
            "Outsider should NOT see any extracts",
        )
        self.assertEqual(
            actions["analysis_rows"],
            [],
            "Outsider should NOT see any analysis rows",
        )


class TestDocumentActionsWithoutCorpus(TestCase):
    """Tests for document actions without corpus context."""

    def setUp(self):
        """Create scenario for document-only queries."""
        self.owner = User.objects.create_user(
            username="owner",
            email="owner@example.com",
            password="testpass123",
        )
        self.viewer = User.objects.create_user(
            username="viewer",
            email="viewer@example.com",
            password="testpass123",
        )

        # Create document without corpus association
        self.document = Document.objects.create(
            title="Standalone Document",
            creator=self.owner,
            is_public=False,
        )

    def test_document_actions_without_corpus_returns_empty_corpus_actions(self):
        """
        GIVEN: A document query without corpus_id
        WHEN: Querying for document actions
        THEN: corpus_actions should be empty (no corpus context)
        """
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=self.owner,
            document_id=self.document.id,
            corpus_id=None,
        )

        self.assertEqual(
            actions["corpus_actions"],
            [],
            "Corpus actions should be empty when no corpus context",
        )

    def test_document_without_permission_returns_empty(self):
        """
        GIVEN: A user without permission to the document
        WHEN: Querying for document actions
        THEN: All fields should be empty
        """
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=self.viewer,
            document_id=self.document.id,
            corpus_id=None,
        )

        self.assertEqual(
            actions["corpus_actions"],
            [],
            "Should return empty corpus_actions",
        )
        self.assertEqual(
            actions["extracts"],
            [],
            "Should return empty extracts",
        )
        self.assertEqual(
            actions["analysis_rows"],
            [],
            "Should return empty analysis_rows",
        )


class TestDocumentActionsIDORProtection(TestCase):
    """Tests for IDOR protection in document actions."""

    def setUp(self):
        """Create scenario for IDOR testing."""
        self.user_a = User.objects.create_user(
            username="user_a",
            email="a@example.com",
            password="testpass123",
        )
        self.user_b = User.objects.create_user(
            username="user_b",
            email="b@example.com",
            password="testpass123",
        )

        # User A's private document
        self.private_document = Document.objects.create(
            title="Private Document",
            creator=self.user_a,
            is_public=False,
        )

    def test_cannot_enumerate_private_documents(self):
        """
        GIVEN: User B who does NOT have permission to User A's document
        WHEN: User B queries for document actions on User A's document
        THEN: Empty results should be returned (same as if document didn't exist)
        """
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=self.user_b,
            document_id=self.private_document.id,
            corpus_id=None,
        )

        # Should return empty dict, not error with "permission denied"
        self.assertEqual(actions["extracts"], [])
        self.assertEqual(actions["corpus_actions"], [])
        self.assertEqual(actions["analysis_rows"], [])

    def test_nonexistent_document_returns_empty(self):
        """
        GIVEN: A non-existent document ID
        WHEN: Querying for document actions
        THEN: Empty results should be returned
        """
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=self.user_a,
            document_id=999999,  # Non-existent
            corpus_id=None,
        )

        self.assertEqual(actions["extracts"], [])
        self.assertEqual(actions["corpus_actions"], [])
        self.assertEqual(actions["analysis_rows"], [])


class TestDocumentActionsAnonymousUser(TestCase):
    """Tests for anonymous user access to document actions."""

    def setUp(self):
        """Create public and private documents."""
        self.owner = User.objects.create_user(
            username="owner",
            email="owner@example.com",
            password="testpass123",
        )

        self.public_document = Document.objects.create(
            title="Public Document",
            creator=self.owner,
            is_public=True,
        )
        self.private_document = Document.objects.create(
            title="Private Document",
            creator=self.owner,
            is_public=False,
        )

        self.public_corpus = Corpus.objects.create(
            title="Public Corpus",
            creator=self.owner,
            is_public=True,
        )

    def test_anonymous_can_access_public_document_actions(self):
        """
        GIVEN: A public document
        WHEN: An anonymous user queries for document actions
        THEN: Should not get permission denied (document is accessible)
        """
        anonymous = AnonymousUser()
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=anonymous,
            document_id=self.public_document.id,
            corpus_id=self.public_corpus.id,
        )

        # Should not fail - just may have empty results if no actions exist
        self.assertIsInstance(actions, dict)
        self.assertIn("corpus_actions", actions)
        self.assertIn("extracts", actions)
        self.assertIn("analysis_rows", actions)

    def test_anonymous_cannot_access_private_document_actions(self):
        """
        GIVEN: A private document
        WHEN: An anonymous user queries for document actions
        THEN: Empty results should be returned
        """
        anonymous = AnonymousUser()
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=anonymous,
            document_id=self.private_document.id,
            corpus_id=None,
        )

        self.assertEqual(actions["corpus_actions"], [])
        self.assertEqual(actions["extracts"], [])
        self.assertEqual(actions["analysis_rows"], [])


class TestDocumentActionsCorpusPermission(TestCase):
    """Tests for corpus permission requirements."""

    def setUp(self):
        """Create scenario with corpus permission requirements."""
        self.owner = User.objects.create_user(
            username="owner",
            email="owner@example.com",
            password="testpass123",
        )
        self.doc_only_user = User.objects.create_user(
            username="doconly",
            email="doconly@example.com",
            password="testpass123",
        )

        # Create corpus and document
        self.corpus = Corpus.objects.create(
            title="Test Corpus",
            creator=self.owner,
            is_public=False,
        )
        self.document = Document.objects.create(
            title="Test Document",
            creator=self.owner,
            is_public=False,
        )

        # Give doc_only_user permission ONLY on document, not corpus
        set_permissions_for_obj_to_user(
            self.doc_only_user,
            self.document,
            [PermissionTypes.READ],
        )
        # Explicitly NOT giving corpus permission

    def test_user_with_only_document_permission_cannot_see_corpus_actions(self):
        """
        GIVEN: A user with READ permission on document but NOT corpus
        WHEN: User queries for document actions with corpus_id
        THEN: Should return empty results (corpus permission required)
        """
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=self.doc_only_user,
            document_id=self.document.id,
            corpus_id=self.corpus.id,
        )

        # Should return empty because corpus permission is required
        self.assertEqual(
            actions["corpus_actions"],
            [],
            "User without corpus permission should NOT see corpus actions",
        )


class TestDocumentActionsSuperuser(TestCase):
    """Tests for superuser access to document actions."""

    def setUp(self):
        """Create scenario for superuser testing."""
        import uuid

        self.superuser = User.objects.create_superuser(
            username=f"admin_{uuid.uuid4().hex[:8]}",
            email=f"admin_{uuid.uuid4().hex[:8]}@example.com",
            password="adminpass123",
        )
        self.owner = User.objects.create_user(
            username="owner",
            email="owner@example.com",
            password="testpass123",
        )

        # Create private corpus and document
        self.corpus = Corpus.objects.create(
            title="Private Corpus",
            creator=self.owner,
            is_public=False,
        )
        self.document = Document.objects.create(
            title="Private Document",
            creator=self.owner,
            is_public=False,
        )

        # Create fieldset and corpus action
        self.fieldset = Fieldset.objects.create(
            name="Test Fieldset",
            description="Test",
            creator=self.owner,
        )
        self.corpus_action = CorpusAction.objects.create(
            name="Test Action",
            corpus=self.corpus,
            fieldset=self.fieldset,
            trigger="add_document",
            creator=self.owner,
        )

    def test_superuser_can_see_all_document_actions(self):
        """
        GIVEN: A superuser
        WHEN: Querying for document actions on private document/corpus
        THEN: All actions should be visible
        """
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=self.superuser,
            document_id=self.document.id,
            corpus_id=self.corpus.id,
        )

        self.assertIn(
            self.corpus_action,
            actions["corpus_actions"],
            "Superuser should see all corpus actions",
        )


class TestGetCorpusActionsForCorpus(TestCase):
    """Tests for get_corpus_actions_for_corpus method."""

    def setUp(self):
        """Create test scenario for corpus actions."""
        self.owner = User.objects.create_user(
            username="owner",
            email="owner@example.com",
            password="testpass123",
        )
        self.reader = User.objects.create_user(
            username="reader",
            email="reader@example.com",
            password="testpass123",
        )
        self.outsider = User.objects.create_user(
            username="outsider",
            email="outsider@example.com",
            password="testpass123",
        )

        # Create corpus
        self.corpus = Corpus.objects.create(
            title="Test Corpus",
            creator=self.owner,
            is_public=False,
        )

        # Give reader READ permission on corpus
        set_permissions_for_obj_to_user(
            self.reader,
            self.corpus,
            [PermissionTypes.READ],
        )

        # Create fieldset and corpus action
        self.fieldset = Fieldset.objects.create(
            name="Test Fieldset",
            description="Test",
            creator=self.owner,
        )
        self.corpus_action = CorpusAction.objects.create(
            name="Test Action",
            corpus=self.corpus,
            fieldset=self.fieldset,
            trigger="add_document",
            creator=self.owner,
        )

    def test_owner_can_see_corpus_actions(self):
        """
        GIVEN: A corpus owner
        WHEN: Querying for corpus actions
        THEN: Actions should be visible
        """
        actions = DocumentActionsQueryOptimizer.get_corpus_actions_for_corpus(
            user=self.owner,
            corpus_id=self.corpus.id,
        )

        self.assertIn(
            self.corpus_action,
            actions,
            "Owner should see corpus actions",
        )

    def test_reader_with_corpus_action_permission_can_see_actions(self):
        """
        GIVEN: A user with READ permission on corpus and corpus action
        WHEN: Querying for corpus actions
        THEN: Actions should be visible
        """
        # Give reader permission on corpus action
        set_permissions_for_obj_to_user(
            self.reader,
            self.corpus_action,
            [PermissionTypes.READ],
        )

        actions = DocumentActionsQueryOptimizer.get_corpus_actions_for_corpus(
            user=self.reader,
            corpus_id=self.corpus.id,
        )

        self.assertIn(
            self.corpus_action,
            actions,
            "Reader with permission should see corpus actions",
        )

    def test_outsider_cannot_see_corpus_actions(self):
        """
        GIVEN: A user without corpus permission
        WHEN: Querying for corpus actions
        THEN: Empty queryset should be returned
        """
        actions = DocumentActionsQueryOptimizer.get_corpus_actions_for_corpus(
            user=self.outsider,
            corpus_id=self.corpus.id,
        )

        self.assertEqual(
            actions.count(),
            0,
            "Outsider should not see corpus actions",
        )

    def test_nonexistent_corpus_returns_empty(self):
        """
        GIVEN: A non-existent corpus ID
        WHEN: Querying for corpus actions
        THEN: Empty queryset should be returned
        """
        actions = DocumentActionsQueryOptimizer.get_corpus_actions_for_corpus(
            user=self.owner,
            corpus_id=999999,  # Non-existent
        )

        self.assertEqual(
            actions.count(),
            0,
            "Non-existent corpus should return empty queryset",
        )

    def test_public_corpus_actions_visible_to_all(self):
        """
        GIVEN: A public corpus with actions
        WHEN: Any user queries for corpus actions
        THEN: Actions should be visible (if user has permission on actions)
        """
        # Make corpus public
        self.corpus.is_public = True
        self.corpus.save()

        # Give outsider permission on corpus action
        set_permissions_for_obj_to_user(
            self.outsider,
            self.corpus_action,
            [PermissionTypes.READ],
        )

        actions = DocumentActionsQueryOptimizer.get_corpus_actions_for_corpus(
            user=self.outsider,
            corpus_id=self.corpus.id,
        )

        self.assertIn(
            self.corpus_action,
            actions,
            "Public corpus actions should be visible",
        )


class TestGetExtractsForDocument(TestCase):
    """Tests for get_extracts_for_document method."""

    def setUp(self):
        """Create test scenario for extracts."""
        from opencontractserver.extracts.models import Extract

        self.owner = User.objects.create_user(
            username="owner",
            email="owner@example.com",
            password="testpass123",
        )
        self.reader = User.objects.create_user(
            username="reader",
            email="reader@example.com",
            password="testpass123",
        )
        self.outsider = User.objects.create_user(
            username="outsider",
            email="outsider@example.com",
            password="testpass123",
        )

        # Create corpus and document
        self.corpus = Corpus.objects.create(
            title="Test Corpus",
            creator=self.owner,
            is_public=False,
        )
        self.document = Document.objects.create(
            title="Test Document",
            creator=self.owner,
            is_public=False,
        )

        # Give reader permission on document
        set_permissions_for_obj_to_user(
            self.reader,
            self.document,
            [PermissionTypes.READ],
        )

        # Create fieldset and extract
        self.fieldset = Fieldset.objects.create(
            name="Test Fieldset",
            description="Test",
            creator=self.owner,
        )
        self.extract = Extract.objects.create(
            name="Test Extract",
            corpus=self.corpus,
            fieldset=self.fieldset,
            creator=self.owner,
        )
        self.extract.documents.add(self.document)

    def test_owner_can_see_extracts_for_document(self):
        """
        GIVEN: A document owner
        WHEN: Querying for extracts that include the document
        THEN: Extracts should be visible
        """
        extracts = DocumentActionsQueryOptimizer.get_extracts_for_document(
            user=self.owner,
            document_id=self.document.id,
        )

        self.assertIn(
            self.extract,
            extracts,
            "Owner should see extracts",
        )

    def test_reader_with_permission_can_see_extracts(self):
        """
        GIVEN: A user with READ permission on document
        WHEN: Querying for extracts
        THEN: Extracts should be visible if user has extract permission
        """
        # Give reader permission on corpus and extract
        set_permissions_for_obj_to_user(
            self.reader,
            self.corpus,
            [PermissionTypes.READ],
        )
        set_permissions_for_obj_to_user(
            self.reader,
            self.extract,
            [PermissionTypes.READ],
        )

        extracts = DocumentActionsQueryOptimizer.get_extracts_for_document(
            user=self.reader,
            document_id=self.document.id,
            corpus_id=self.corpus.id,
        )

        self.assertIn(
            self.extract,
            extracts,
            "Reader with permission should see extracts",
        )

    def test_outsider_cannot_see_extracts(self):
        """
        GIVEN: A user without document permission
        WHEN: Querying for extracts
        THEN: Empty queryset should be returned
        """
        extracts = DocumentActionsQueryOptimizer.get_extracts_for_document(
            user=self.outsider,
            document_id=self.document.id,
        )

        self.assertEqual(
            extracts.count(),
            0,
            "Outsider should not see extracts",
        )

    def test_nonexistent_document_returns_empty(self):
        """
        GIVEN: A non-existent document ID
        WHEN: Querying for extracts
        THEN: Empty queryset should be returned
        """
        extracts = DocumentActionsQueryOptimizer.get_extracts_for_document(
            user=self.owner,
            document_id=999999,  # Non-existent
        )

        self.assertEqual(
            extracts.count(),
            0,
            "Non-existent document should return empty queryset",
        )


class TestGetAnalysisRowsForDocument(TestCase):
    """Tests for get_analysis_rows_for_document method."""

    def setUp(self):
        """Create test scenario for analysis rows."""
        from opencontractserver.analyzer.models import Analysis, Analyzer
        from opencontractserver.documents.models import DocumentAnalysisRow

        self.owner = User.objects.create_user(
            username="owner",
            email="owner@example.com",
            password="testpass123",
        )
        self.reader = User.objects.create_user(
            username="reader",
            email="reader@example.com",
            password="testpass123",
        )
        self.outsider = User.objects.create_user(
            username="outsider",
            email="outsider@example.com",
            password="testpass123",
        )

        # Create corpus and document
        self.corpus = Corpus.objects.create(
            title="Test Corpus",
            creator=self.owner,
            is_public=False,
        )
        self.document = Document.objects.create(
            title="Test Document",
            creator=self.owner,
            is_public=False,
        )

        # Give reader permission on document
        set_permissions_for_obj_to_user(
            self.reader,
            self.document,
            [PermissionTypes.READ],
        )

        # Create analyzer and analysis
        # Note: Analyzer has a constraint requiring either host_gremlin or task_name
        self.analyzer = Analyzer.objects.create(
            id="test.analyzer.for.document.actions",
            description="Test Analyzer",
            task_name="test.task.name",
            creator=self.owner,
        )
        self.analysis = Analysis.objects.create(
            analyzer=self.analyzer,
            analyzed_corpus=self.corpus,
            creator=self.owner,
        )

        # Create analysis row
        # Note: `data` field is a ManyToManyField to Datacell, not a JSON field
        self.analysis_row = DocumentAnalysisRow.objects.create(
            document=self.document,
            analysis=self.analysis,
            creator=self.owner,
        )

    def test_owner_can_see_analysis_rows(self):
        """
        GIVEN: A document owner
        WHEN: Querying for analysis rows
        THEN: Analysis rows should be visible
        """
        rows = DocumentActionsQueryOptimizer.get_analysis_rows_for_document(
            user=self.owner,
            document_id=self.document.id,
        )

        self.assertIn(
            self.analysis_row,
            rows,
            "Owner should see analysis rows",
        )

    def test_reader_with_permission_can_see_analysis_rows(self):
        """
        GIVEN: A user with READ permission on document and analysis
        WHEN: Querying for analysis rows
        THEN: Analysis rows should be visible
        """
        # Give reader permission on corpus and analysis
        set_permissions_for_obj_to_user(
            self.reader,
            self.corpus,
            [PermissionTypes.READ],
        )
        set_permissions_for_obj_to_user(
            self.reader,
            self.analysis,
            [PermissionTypes.READ],
        )

        rows = DocumentActionsQueryOptimizer.get_analysis_rows_for_document(
            user=self.reader,
            document_id=self.document.id,
            corpus_id=self.corpus.id,
        )

        self.assertIn(
            self.analysis_row,
            rows,
            "Reader with permission should see analysis rows",
        )

    def test_outsider_cannot_see_analysis_rows(self):
        """
        GIVEN: A user without document permission
        WHEN: Querying for analysis rows
        THEN: Empty queryset should be returned
        """
        rows = DocumentActionsQueryOptimizer.get_analysis_rows_for_document(
            user=self.outsider,
            document_id=self.document.id,
        )

        self.assertEqual(
            rows.count(),
            0,
            "Outsider should not see analysis rows",
        )

    def test_nonexistent_document_returns_empty(self):
        """
        GIVEN: A non-existent document ID
        WHEN: Querying for analysis rows
        THEN: Empty queryset should be returned
        """
        rows = DocumentActionsQueryOptimizer.get_analysis_rows_for_document(
            user=self.owner,
            document_id=999999,  # Non-existent
        )

        self.assertEqual(
            rows.count(),
            0,
            "Non-existent document should return empty queryset",
        )

    def test_analysis_rows_include_select_related(self):
        """
        GIVEN: A document with analysis rows
        WHEN: Querying for analysis rows
        THEN: Related objects should be prefetched (analysis, analyzer)
        """
        rows = DocumentActionsQueryOptimizer.get_analysis_rows_for_document(
            user=self.owner,
            document_id=self.document.id,
        )

        # Access related objects - should not trigger additional queries
        for row in rows:
            # These accesses should work without additional queries
            self.assertIsNotNone(row.analysis)
            self.assertIsNotNone(row.analysis.analyzer)


class TestDocumentActionsNonexistentCorpus(TestCase):
    """Tests for handling non-existent corpus in get_document_actions."""

    def setUp(self):
        """Create test scenario."""
        self.owner = User.objects.create_user(
            username="owner",
            email="owner@example.com",
            password="testpass123",
        )
        self.document = Document.objects.create(
            title="Test Document",
            creator=self.owner,
            is_public=False,
        )

    def test_nonexistent_corpus_id_still_returns_document_results(self):
        """
        GIVEN: A valid document but non-existent corpus ID
        WHEN: Querying for document actions
        THEN: Should still return document-level results (extracts, analysis rows)
        """
        actions = DocumentActionsQueryOptimizer.get_document_actions(
            user=self.owner,
            document_id=self.document.id,
            corpus_id=999999,  # Non-existent corpus
        )

        # Should return empty corpus_actions but not fail
        self.assertEqual(actions["corpus_actions"], [])
        # Document-level results still returned (may be empty if no extracts/rows)
        self.assertIsInstance(actions["extracts"], list)
        self.assertIsInstance(actions["analysis_rows"], list)
